/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package genomemap.data;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.UnmodifiableIterator;
import commons.core.Data;
import commons.core.MutableData;
import commons.util.ArrayUtil;
import commons.util.BinaryDataUtil;
import commons.util.ExceptionUtil;
import genomemap.data.KSA00Data.Hybridization;
import genomemap.model.KSA00;

import java.util.*;

/**
 * @author Susanta Tewari
 * @since Jul 30, 2011
 */
public class KSA00Data extends Data<KSA00, KSA00Data> implements Iterable<Hybridization> {

    /** Field description */
    private ImmutableMap<String, Hybridization> cloneHDs;

    /**
     * @param m model
     * @param labelledHDScores key: clone value: hybridization score
     */
    public KSA00Data(final KSA00 m, final Map<String, String> labelledHDScores) {

        super(m);


        // create an order for the labels
        final List<String> labels = new ArrayList<>(labelledHDScores.keySet());


        // using the label order, create scores
        final byte[][] scores = BinaryDataUtil.getBinaryData(labels, labelledHDScores);

        init(labels, scores);
    }

    /**
     * Constructs ...
     *
     *
     * @param m
     * @param cloneLabels
     * @param scores
     */
    public KSA00Data(final KSA00 m, final List<String> cloneLabels, final byte[][] scores) {

        super(m);

        init(cloneLabels, scores);
    }

    /**
     * Method description
     *
     *
     * @param cloneLabels
     * @param scores
     */
    private void init(final List<String> cloneLabels, final byte[][] scores) {

        final Map<String, Hybridization> map = new HashMap<>(scores.length);

        for (int i = 0; i < scores.length; i++) {

            final String clone = cloneLabels.get(i);

            map.put(clone, new Hybridization(clone, scores[i]));
        }

        cloneHDs = ImmutableMap.copyOf(map);
    }

    /**
     * @return total number of hybridizations present
     */
    public int countHybridization() {

        int result = 0;

        for (final String clone : cloneHDs.keySet()) {
            result += cloneHDs.get(clone).countHybridization();
        }

        return result;
    }

    /**
     * @return clones corresponding to each sample point
     */
    public ImmutableSet<String> getClones() {
        return cloneHDs.keySet();
    }

    /**
     * @param clone a clone in the data set
     * @return Hybridization for the given clone
     */
    private Hybridization getHybridization(final String clone) {

        checkCloneArgument(clone);

        return cloneHDs.get(clone);
    }

    /**
     * Method description
     *
     *
     * @return
     */
    @Override
    public MutableData<KSA00, KSA00Data> getMutable() {

        return new MutableData<KSA00, KSA00Data>(getModel()) {

            @Override
            public KSA00Data getImmutable() {

                final KSA00Data old_data           = KSA00Data.this;
                final int[] probeIndex = ArrayUtil.indicesOf(old_data.getModel().getProbes(),
                                             getModel().getProbes());
                final List<String> old_cloneLabels = new ArrayList<>(old_data.getClones());
                final byte[][] new_scores          = new byte[old_cloneLabels.size()][];

                for (int i = 0; i < old_cloneLabels.size(); i++) {

                    final String clone = old_cloneLabels.get(i);

                    new_scores[i] = old_data.getHybridization(clone).getScore(probeIndex);
                }

                return new KSA00Data(getModel(), old_cloneLabels, new_scores);
            }
        };
    }

    /**
     * Method description
     *
     *
     * @return
     */
    @Override
    public UnmodifiableIterator<Hybridization> iterator() {
        return cloneHDs.values().iterator();
    }

    /**
     * @param clone clone
     * @param probe probe
     * @return {@code true} if the specified probe hybridized to the specified clone; {@code false}
     *         otherwise
     */
    public boolean isHybridizing(final String clone, final String probe) {

        checkCloneArgument(clone);

        final int probeIndex = getProbeIndex(probe);

        return cloneHDs.get(clone).isConnecting(probeIndex);
    }

    /**
     * The probe itself is excluded.
     *
     * @param probe probe whose connecting clones are requested. The probe itself is not considered
     * among such clones
     * @return connecting clones for the specified probe
     */
    public Set<String> getConnectingClones(final String probe) {

        final int probeIndex     = getProbeIndex(probe);
        final Set<String> result = new HashSet<>(10);

        for (final String clone : cloneHDs.keySet()) {


            // skip for the probe
            if (clone.equals(probe)) {
                continue;
            }


            // consider all clones other than the probe specified
            if (cloneHDs.get(clone).isConnecting(probeIndex)) {
                result.add(clone);
            }
        }

        return result;
    }

    /**
     * @param probe1 the first of the two probes whose connecting clones are requested
     * @param probe2 the second of the two probes whose connecting clones are requested
     * @return connecting clones of the specified probes
     */
    public Set<String> getConnectingClones(final String probe1, final String probe2) {

        final int probe1Index    = getProbeIndex(probe1);
        final int probe2Index    = getProbeIndex(probe2);
        final Set<String> result = new HashSet<>(10);

        for (final String clone : cloneHDs.keySet()) {

            if (cloneHDs.get(clone).isConnecting(probe1Index, probe2Index)) {
                result.add(clone);
            }
        }

        return result;
    }

    /**
     * Method description
     *
     *
     * @param clone
     */
    private void checkCloneArgument(final String clone) {

        if (!cloneHDs.containsKey(clone)) {
            ExceptionUtil.throwIllegalArgEx(getClass(), "KSA00Data_Argument_Clone_Not_Found");
        }
    }

    /**
     * @param probe probe whose index is to be found
     * @return index (0 based) of the specified probe in the model
     */
    private int getProbeIndex(final String probe) {

        final int probeIndex = getModel().getProbes().indexOf(probe);

        if (probeIndex == -1) {
            ExceptionUtil.throwIllegalArgEx(getClass(), "KSA00Data_Argument_Probe_Not_Found");
        }

        return probeIndex;
    }

    /**
     * Total # of clones. Every clone constitutes a sample element.
     *
     * @return clone count
     */
    @Override
    public int getSampleSize() {
        return cloneHDs.size();
    }

    /**
     * Class description
     *
     *
     * @version        Enter version here..., 12/10/09
     * @author         Susanta Tewari
     */
    public class Hybridization {

        /** Field description */
        private final byte[] score;

        /** Field description */
        private final String clone;

        /**
         * Constructs ...
         *
         *
         * @param clone
         * @param score
         */
        public Hybridization(final String clone, final byte[] score) {

            this.clone = clone;
            this.score = score;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public int length() {
            return score.length;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public String getClone() {
            return clone;
        }

        /**
         * Method description
         *
         *
         * @return
         */
        public byte[] getScore() {
            return score;
        }

        /**
         * Indices in probeIndex correspond to a subset of probes.
         *
         * @param probeIndices indices of probes whose scores are requested
         * @return scores array of the specified probe indices
         */
        private byte[] getScore(final int[] probeIndices) {

            final byte[] result = new byte[probeIndices.length];

            for (int i = 0; i < result.length; i++) {
                result[i] = score[probeIndices[i]];
            }

            return result;
        }

        /**
         * @return number of hybridizations present
         */
        private int countHybridization() {

            int result = 0;

            for (final byte bit : score) {
                if (bit == 1) result++;
            }

            return result;
        }

        /**
         * @param probeIndex index of probe whose connection (hybridization) with the clone
         * (represented by this object) is tested
         * @return {@code true} if connected; {@code false} otherwise
         */
        private boolean isConnecting(final int probeIndex) {
            return score[probeIndex] == 1;
        }

        /**
         * Method description
         *
         *
         * @param probe1Index
         * @param probe2Index
         *
         * @return
         */
        private boolean isConnecting(final int probe1Index, final int probe2Index) {
            return (score[probe1Index] == 1) && (score[probe2Index] == 1);
        }

        /**
         * Method description
         *
         *
         * @param obj
         *
         * @return
         */
        @SuppressWarnings("AccessingNonPublicFieldOfAnotherObject")
        @Override
        public boolean equals(final Object obj) {

            if (obj == null) {
                return false;
            }

            if (getClass() != obj.getClass()) {
                return false;
            }

            final Hybridization other = (Hybridization) obj;

            return Arrays.equals(this.score, other.score);
        }

        /**
         * Method description
         *
         *
         * @return
         */
        @Override
        public int hashCode() {

            int hash = 7;

            hash = 17 * hash + Arrays.hashCode(this.score);

            return hash;
        }
    }
}
